﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Xtensible.Extensions.Mapping
{
    /// <summary>
    ///   Map Actions for enumerations.  Method names mirror the names used in MapAction (for single objects) to create a more natural API.
    ///   A call to ForGroup() on an enumeration (via extension method) is what brings EnumerationMapAction into play (vs MapAction)
    /// </summary>
    public class EnumerationMapAction : MapAction
    {
        internal IEnumerable<object> SourceEnumaration { get; set; }

        /// <summary>
        ///   Maps a source list to a destination list.  Destination list must have a public default constructor since new objects are created automatically
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <returns></returns>
        public new IEnumerable<T> Map<T>() where T : new()
        {
            if (Source == null)
                return null;
            else
            {
                return SourceEnumaration.Select(
                    o =>
                        {
                            var destination = new MapAction
                                                  {
                                                      Source = o,
                                                      SourceType = o.GetType(),
                                                      Aliases = Aliases,
                                                      DataTypeTriggers = DataTypeTriggers,
                                                      Exclusions = Exclusions,
                                                      PropertyNameTriggers = PropertyNameTriggers,
                                                      FlattenExclusions = FlattenExclusions,
                                                      FlattenPrefixes = FlattenPrefixes,
                                                      FlatteningFunc = FlatteningFunc
                                                  };
                            return destination.Map<T>();
                        }).ToList();
            }
        }

        /// <summary>
        ///   Cannot be used for enumeration
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "destination"></param>
        /// <returns></returns>
        [Obsolete("Cannot be used for enumeration", true)]
        public T Map<T>(T destination)
        {
            throw new InvalidOperationException("Cannot be used for enumeration");
        }

        /// <summary>
        ///   Maps source properties to destination properties with a different name
        /// </summary>
        /// <param name = "sourceName">Source property name</param>
        /// <param name = "destinationName">Destination property name</param>
        /// <returns></returns>
        public EnumerationMapAction WithAlias(string destinationName, string sourceName)
        {
            if (Aliases == null)
                Aliases = new Aliases();

            Aliases.Add(destinationName, sourceName);

            return this;
        }

        /// <summary>
        ///   Combines WithAlias and WithPropertyNameTrigger; it maps source properties with destination properties with a different name and then applies trigger rules
        /// </summary>
        /// <param name = "?"></param>
        /// <param name = "destinationName"></param>
        /// <param name = "sourceName"></param>
        /// <param name="triggerAction"></param>
        /// <returns></returns>
        public EnumerationMapAction WithAliasTrigger(string destinationName, string sourceName,
                                                     Func<object, object> triggerAction)
        {
            return WithAlias(destinationName, sourceName).WithPropertyNameTrigger(sourceName, triggerAction);
        }

        /// <summary>
        ///   Combines WithAlias and WithPropertyNameTrigger; it maps source properties with destination properties with a different name and then applies trigger rules
        /// </summary>
        /// <typeparam name = "D"></typeparam>
        /// <typeparam name = "S"></typeparam>
        /// <param name = "destinationExpression"></param>
        /// <param name = "sourceExpression"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public EnumerationMapAction WithAliasTrigger<D, S>(Expression<Func<D, object>> destinationExpression,
                                                           Expression<Func<S, object>> sourceExpression,
                                                           Func<object, object> triggerAction)
        {
            return WithAlias(destinationExpression, sourceExpression).WithPropertyNameTrigger(sourceExpression,
                                                                                              triggerAction);
        }

        /// <summary>
        ///   Maps source properties to destination properties with a different name
        /// </summary>
        /// <typeparam name = "S"></typeparam>
        /// <typeparam name = "D"></typeparam>
        /// <param name = "o"></param>
        /// <param name = "sourceExpression"></param>
        /// <param name = "destinationExpression"></param>
        /// <returns></returns>
        public EnumerationMapAction WithAlias<D, S>(Expression<Func<D, object>> destinationExpression,
                                                    Expression<Func<S, object>> sourceExpression)
        {
            try
            {
                var destination = Helper.GetPropertyNameFromExpression(destinationExpression);
                var source = Helper.GetPropertyNameFromExpression(sourceExpression);

                if (Aliases == null)
                    Aliases = new Aliases();

                Aliases.Add(destination, source);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Only properties can be aliased", ex);
            }

            return this;
        }

        /// <summary>
        ///   Transforms data before copying to destination for specified type
        /// </summary>
        /// <param name = "type"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public EnumerationMapAction WithTypeTrigger(Type type, Func<object, object> triggerAction)
        {
            if (DataTypeTriggers == null)
                DataTypeTriggers = new DataTypeTriggers();

            DataTypeTriggers.Add(type, triggerAction);

            return this;
        }

        /// <summary>
        ///   Transforms data before copying to destination for specified property
        /// </summary>
        /// <param name = "propertyName"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public EnumerationMapAction WithPropertyNameTrigger(string propertyName, Func<object, object> triggerAction)
        {
            if (PropertyNameTriggers == null)
                PropertyNameTriggers = new PropertyNameTriggers();

            PropertyNameTriggers.Add(propertyName, triggerAction);

            return this;
        }

        /// <summary>
        ///   Transforms data before copying to destination for specified property
        /// </summary>
        /// <typeparam name = "S"></typeparam>
        /// <param name = "sourceExpression"></param>
        /// <param name = "triggerAction"></param>
        /// <returns></returns>
        public EnumerationMapAction WithPropertyNameTrigger<S>(Expression<Func<S, object>> sourceExpression,
                                                               Func<object, object> triggerAction)
        {
            try
            {
                var source = Helper.GetPropertyNameFromExpression(sourceExpression);

                if (PropertyNameTriggers == null)
                    PropertyNameTriggers = new PropertyNameTriggers();

                PropertyNameTriggers.Add(source, triggerAction);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }

            return this;
        }

        /// <summary>
        ///   Excludes copying of data for specified property names
        /// </summary>
        /// <param name = "propertyName"></param>
        /// <returns></returns>
        public EnumerationMapAction WithExclusion(string propertyName)
        {
            if (Exclusions == null)
                Exclusions = new Exclusions();

            Exclusions.Add(propertyName);

            return this;
        }

        /// <summary>
        ///   Excludes copying of data for specified property names
        /// </summary>
        /// <returns></returns>
        public EnumerationMapAction WithExclusion<S>(Expression<Func<S, object>> sourceExpression)
        {
            try
            {
                var source = Helper.GetPropertyNameFromExpression(sourceExpression);

                if (Exclusions == null)
                    Exclusions = new Exclusions();

                Exclusions.Add(source);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Only properties can be excluded", ex);
            }

            return this;
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <returns></returns>
        public EnumerationMapAction Flatten(string sourceProperty)
        {
            return Flatten(sourceProperty, sourceProperty);
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <returns></returns>
        public EnumerationMapAction Flatten<S>(Expression<Func<S, object>> sourceExpression)
        {
            try
            {
                var propertyName = Helper.GetPropertyNameFromExpression(sourceExpression);

                return Flatten(propertyName);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName if the prefix of "Sup" is provided
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <param name = "destinationPropertyPrefix"></param>
        /// <returns></returns>
        public EnumerationMapAction Flatten(string sourceProperty, string destinationPropertyPrefix)
        {
            return Flatten(sourceProperty, destinationPropertyPrefix, null);
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <returns></returns>
        public EnumerationMapAction Flatten<S>(Expression<Func<S, object>> sourceExpression,
                                               string destinationPropertyPrefix)
        {
            try
            {
                var propertyName = Helper.GetPropertyNameFromExpression(sourceExpression);

                return Flatten(propertyName, destinationPropertyPrefix);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName if the prefix of "Sup" is provided
        /// </example>
        /// <param name = "sourceProperty"></param>
        /// <param name = "destinationPropertyPrefix"></param>
        /// <param name = "excludedDestinationProperties">Excluded destination properties will not be mapped</param>
        /// <returns></returns>
        public EnumerationMapAction Flatten(string sourceProperty, string destinationPropertyPrefix,
                                            List<string> excludedDestinationProperties)
        {
            if (FlattenPrefixes == null)
                FlattenPrefixes = new Dictionary<string, string>();
            FlattenPrefixes.Add(sourceProperty, destinationPropertyPrefix);

            if (excludedDestinationProperties != null)
            {
                if (FlattenExclusions == null)
                    FlattenExclusions = new Dictionary<string, List<string>>();

                FlattenExclusions.Add(sourceProperty, excludedDestinationProperties);
            }
            return this;
        }

        /// <summary>
        ///   Flattens 2nd level objects based on naming convention.
        /// </summary>
        /// <example>
        ///   Assuming you have a destination class with SupervisorFirstName, 
        ///   source object employee with a child object of Supervisor, which contains property FirstName - SupervisorFirstName will be mapped with the value
        ///   of employee.Supervisor.FirstName
        /// </example>
        /// <returns></returns>
        public EnumerationMapAction Flatten<S>(Expression<Func<S, object>> sourceExpression,
                                               string destinationPropertyPrefix,
                                               List<string> excludedDestinationProperties)
        {
            try
            {
                var propertyName = Helper.GetPropertyNameFromExpression(sourceExpression);

                return Flatten(propertyName, destinationPropertyPrefix, excludedDestinationProperties);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cannot determine property from expression", ex);
            }
        }


        /// <summary>
        ///   Custom or complex flattening rules can be specified here.  This runs before any other flattening procedure.
        /// </summary>
        /// <param name = "flattenFunc"></param>
        /// <returns></returns>
        public EnumerationMapAction Flatten(Func<object, object> flattenFunc)
        {
            if (FlatteningFunc != null)
                throw new InvalidOperationException("FlattenFunc can only be set once");

            FlatteningFunc = flattenFunc;

            return this;
        }
    }
}